<?php
/**     
  *
  * Copyright (c) 2009, Persistent Systems Limited
  *
  * Redistribution and use, with or without modification, are permitted 
  *  provided that the following  conditions are met:
  *   - Redistributions of source code must retain the above copyright notice, 
  *     this list of conditions and the following disclaimer.
  *   - Neither the name of Persistent Systems Limited nor the names of its contributors 
  *     may be used to endorse or promote products derived from this software 
  *     without specific prior written permission.
  *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
/**
 *  For getting DotNetService Environment variables
 */ 
require_once 'DotNetServicesEnvironment.php';

/**
 *  For ACS constants
 */ 
require_once 'ACS/ACSConstants.php';

/**
 *  For getting SimpleApiAuthService Token properties and methods
 */ 
require_once 'ACS/SimpleApiAuthService.php';

/**
 * Class TokenValidator for validating SimpleApiAuth Token 
 * @access public
 */
class TokenValidator
{
    /**
     * @access private
     * @var string
     */
    private $_trustedTokenIssuer;

    /**
     * @access private
     * @var string
     */
    private $_trustedAudience;

    /**
     * @access private
     * @var string
     */
    private $_trustedSigningKey;

    /**
     * @access private
     * @var string
     */
    private $_token;

    /**
     * @access private
     * @var string
     */
    private $_claims;
    
    /**
     * Sets the member variables
     * @param string $trustedService It is the service name
     * @param string $trustedAudience It is the trusted audience.
     * @param string $trustedSigningKey It is the signing key.
     * @param string $token Token from ACS service.
     */    
    public function __construct($trustedService, $trustedAudience, $trustedSigningKey, $token)
    {
        $this->_trustedTokenIssuer = 'https://' . $trustedService . 
            '.' . DotNetServicesEnvironment::getACMHostName() . '/';
        $this->_trustedAudience = $trustedAudience;
        $this->_trustedSigningKey = $trustedSigningKey;
        $this->_token = $token;
    }

    /**
     * Validates the token
     * @return boolean 
     */    
    public function validate()
    {
        if (stripos($this->_token, WRAP_AUTHENTICATION_TYPE) != 0)
        {
            // Must start with WRAP_AUTHENTICATION_TYPE
            return false;
        }

        $headerParts = explode(' ', $this->_token);
        if (count($headerParts) != 2)
        {
            // Must have only two parts
            return false;
        }
        
        $parts = explode('=', $headerParts[1], 2);
        if ($parts[0] != WRAP_AUTHORIZATION_HEADER_KEY)
        {
            // This part must be WRAP_AUTHORIZATION_HEADER_KEY 
            return false;
        }
        if (count($parts) != 2)
        {
            // Must have only two parts
            return false;
        }
        
        // Second part must be enclosed in double quotes
        if (stripos($parts[1], '"') != 0)
        {
            return false;
        }
        if (strripos($parts[1], '"') != strlen($parts[1]) - 1)
        {
            return false;
        }
        
        // Get raw token inside double quotes
        $acsToken =  substr($parts[1], 1, strlen($parts[1]) - 2);
        
        if (!$this->isHMACValid($acsToken))
        {
            return false;
        }
        
        $this->populateClaims($acsToken);        
        
        if ($this->isExpired($acsToken))
        {
            return false;
        }
        
        if (!$this->isIssuerTrusted($acsToken))
        {
            return false;
        }
        
        if (!$this->isAudienceTrusted($acsToken))
        {
            return false;
        }
        
        return true;
    }
    
    /**
     * Checks the token against hmac key
     * @return boolean
     */    
    private function isHMACValid($tokenValue)
    {
        $tokenSignatures = explode('&' . TOKEN_DIGEST_256 . '=', $tokenValue);
        if (count($tokenSignatures) != 2)
        {
            return false;
        }
        
        // Compute signature on parameters
        $computedSignature = base64_encode(hash_hmac('sha256', $tokenSignatures[0], base64_decode($this->_trustedSigningKey), true));
        
        // Compare computed signature with signature provided in token
        if ($computedSignature == urldecode($tokenSignatures[1]))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    /**
     * Extracts the claims from token
     * @return boolean
     */    
    private function populateClaims($tokenValue)
    {
        $params = explode('&', $tokenValue);
        $this->_claims = array();
        foreach ($params as $param)
        {
            if (!empty($param))
            {
                $namevalue = explode('=', $param);
                if (count($namevalue) != 2)
                {
                    throw new Exception("Missing = characters in the token");
                }
                
                if (!empty($this->_claims[$namevalue[0]]))
                {
                    throw new Exception("Repeated name/value pair in the token");
                }
                
                $this->_claims[$namevalue[0]] = urldecode($namevalue[1]);
            }
        }        
    }
    
    /**
     * Checks for expiration of token
     * @return boolean
     */    
    private function isExpired()
    {
        if (empty($this->_claims))
        {
            throw new Exception("Claims not defined in the token.");
        }
        
        $expiresOn =  $this->_claims[TOKEN_EXPIRES_ON];
        $currentTime = time();
        
        // Current timestamp must be less than expiration time stamp
        if (empty($expiresOn) || ($currentTime > $expiresOn))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    /**
     * Checks for trusted issuer
     * @return boolean
     */    
    private function isIssuerTrusted()
    {
        if (empty($this->_claims))
        {
            throw new Exception("Claims not defined in the token.");
        }
        
        $issuer = $this->_claims[TOKEN_ISSUER];        
        if (empty($issuer) || (strcasecmp($issuer, $this->_trustedTokenIssuer)  != 0))
        {
            return false;
        }
        else
        {
            return true;
        }
    }
    
    /**
     * Checks for trusted audience
     * @return boolean
     */    
    private function isAudienceTrusted()
    {
        if (empty($this->_claims))
        {
            throw new Exception("Claims not defined in the token.");
        }
        
        $audience = $this->_claims[TOKEN_AUDIENCE];
        if (empty($audience) || ($audience != $this->_trustedAudience))
        {
            return false;
        }
        else
        {
            return true;
        }
    }

    /**
     * returns the claims
     * @return array
     */    
    public function getClaims()
    {
        return $this->_claims;
    }
}
?>
